如果要一次塞幾萬筆資料給 Redis,一問一答的網路來回延遲絕對會讓你等到睡著。
Redis 為了解這個問題提供了 Pipeline (管道)。今天就來看之前寫好的 TCP Server,能不能不用大改就吃下 Pipeline。
Pipeline 說穿了就是把多個命令先排在一起送出去,Server 再照順序解析、執行、回覆。重點不是單條命令變快,而是減少一問一答的網路等待。
沒有 Pipeline:
Client ---- SET a 1 ----> Server (執行)
Client <--- OK ---------- Server
Client ---- SET b 2 ----> Server (執行)
Client <--- OK ---------- Server
使用 Pipeline:
Client ---- SET a 1 \r\n SET b 2 ----> Server (執行 a, 執行 b)
Client <--- OK \r\n OK --------------- Server
用了 Pipeline,多個命令不用各自等一次 RTT,批量寫入時差距會很明顯。
在 Day 2 實作的 TCP 伺服器與 Day 13 整合的 handleConnection 中,我的程式碼是這樣寫的:
// 使用 RESP Reader 解析客戶端傳來的資料
reader := resp.NewReader(conn)
for {
val, err := reader.ReadValue()
if err != nil {
break
}
// 透過dispatcher 執行命令
reply := s.dispatcher.Dispatch(clientCtx, val)
// 如果有回覆,寫回 conn
if reply.Type != 0 {
_, err = conn.Write(reply.Marshal())
if err != nil {
break
}
}
}
這個 Loop 看似是順序的,但正因為我底層使用了 bufio.Reader,它在進行系統呼叫 Read 時,會將 TCP 接收緩衝區中的大量資料一次性讀取到記憶體緩衝區中(預設為 4096 位元組)。
說實在,寫 Day 2 網路層的時候我還沒想那麼遠,只是直覺加上了 bufio.Reader。直到今天測試大量塞入資料,才發現原來 Go 的 bufio 早就幫我把 Pipeline 接收端需要的一次性讀取做掉了,有一種賺到的感覺。
當客戶端使用 Pipeline 一次性發送了 10 條命令時:
net.Conn.Read 一次性將 10 條命令的 RESP 位元組讀入 bufio.Reader 緩衝區。for 迴圈調用 reader.ReadValue(),它會從記憶體緩衝區中連續解析出 10 個 Value,少掉不少額外的系統呼叫(Syscall)。conn。所以雖然我們還沒有專門為 Pipeline 寫批次回覆邏輯,接收和解析這一段其實已經能吃下連續命令。
bufio.Writer)目前,我們每次執行完命令,都直接呼叫 conn.Write 寫回客戶端。在高併發小命令的 Pipeline 場景下,這會觸發多次 write 系統呼叫,增加核心態(Kernel Mode)與用戶態(User Mode)切換的開銷。
如果後面要再優化,可以在連線寫入端加上 bufio.Writer:
bufio.Reader 的內部緩衝區中沒有更多可讀資料時(即當前批次命令已全部執行完畢),主動呼叫 writer.Flush() 將所有回覆一次性推回給客戶端。這類緩衝寫入也是 Redis 做網路 I/O 優化時會考慮的方向。之後如果要壓效能,可以把這塊補上。
先把 Server 跑起來:
go run ./code/main.go
我們可以用 printf 送 INFO 指令來看看伺服器目前的狀態。我們來實測一下:
# INFO
printf "*1\r\n\$4\r\nINFO\r\n" | nc localhost 6379
# 預期回覆:
# $...
# # Server
# redis_version:999.999.999
# os:Darwin
# arch_bits:64
#
# # Clients
# connected_clients:1
#
# # Memory
# used_memory:1048576
#
# # Persistence
# aof_enabled:0
今天把 Pipeline 的接收流程看了一輪,也確認之前放的 bufio.Reader 剛好幫上忙。結果這邊為了算準記憶體使用量卡了老半天,還得拿 runtime.ReadMemStats 一個一個慢慢推算,Go 的垃圾回收機制有時候真的會讓這個數字跳來跳去的。
明天開始進到資料持久化,先寫 AOF 追加日誌。感覺那塊會有點麻煩。